/*
*Copyright 2021-2023 NERCIS
*
*Licensed under the Apache License, Version 2.0 (the "License");
*you may not use this file except in compliance with the License.
*You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*Unless required by applicable law or agreed to in writing, software
*distributed under the License is distributed on an "AS IS" BASIS,
*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*See the License for the specific language governing permissions and
*limitations under the License.
*/

package cn.ac.nercis.pes.community.component.controls;

import impl.org.controlsfx.collections.MappingChange;
import impl.org.controlsfx.collections.NonIterableChange;
import impl.org.controlsfx.collections.ReadOnlyUnbackedObservableList;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.controlsfx.control.IndexedCheckModel;

import java.util.BitSet;
import java.util.Map;

/**
 * CheckBitSetModelBase
 * @author zpy
 * @version 1.0.0
 */
abstract class CheckBitSetModelBase<T> implements IndexedCheckModel<T> {
    private final Map<T, BooleanProperty> itemBooleanMap;

    private final BitSet checkedIndices;
    private final ReadOnlyUnbackedObservableList<Integer> checkedIndicesList;
    private final ReadOnlyUnbackedObservableList<T> checkedItemsList;
    CheckBitSetModelBase(final Map<T, BooleanProperty> itemBooleanMap) {
        this.itemBooleanMap = itemBooleanMap;

        this.checkedIndices = new BitSet();

        this.checkedIndicesList = new ReadOnlyUnbackedObservableList<>() {
            @Override
            public Integer get(int index) {
                if (index < 0 || index >= getItemCount()) {
                    return -1;
                }

                for (int pos = 0, val = checkedIndices.nextSetBit(0);
                     val >= 0 || pos == index;
                     pos++, val = checkedIndices.nextSetBit(val + 1)) {
                    if (pos == index) {
                        return val;
                    }
                }

                return -1;
            }

            @Override
            public int size() {
                return checkedIndices.cardinality();
            }

            @Override
            public boolean contains(Object o) {
                if (o instanceof Number n) {
                    int index = n.intValue();
                    return index >= 0 && index < checkedIndices.length() &&
                            checkedIndices.get(index);
                }
                return false;
            }
        };

        this.checkedItemsList = new ReadOnlyUnbackedObservableList<T>() {
            @Override public T get(int i) {
                int pos = checkedIndicesList.get(i);
                if (pos < 0 || pos >= getItemCount()) {
                    return null;
                }
                return getItem(pos);
            }

            @Override public int size() {
                return checkedIndices.cardinality();
            }
        };

        final MappingChange.Map<Integer,T> map = this::getItem;

        checkedIndicesList.addListener((ListChangeListener<Integer>) c -> {
            // when the selectedIndices ObservableList changes, we manually call
            // the observers of the selectedItems ObservableList.
            boolean hasRealChangeOccurred = false;
            while (c.next() && ! hasRealChangeOccurred) {
                hasRealChangeOccurred = c.wasAdded() || c.wasRemoved();
            }

            if (hasRealChangeOccurred) {
                c.reset();
                checkedItemsList.callObservers(new MappingChange<>(c, map, checkedItemsList));
            }
            c.reset();
        });

        // this code is to handle the situation where a developer is manually
        // toggling the check model, and expecting the UI to update (without
        // this it won't happen!).
        getCheckedItems().addListener((ListChangeListener<T>) c -> {
            while (c.next()) {
                if (c.wasAdded()) {
                    for (T item : c.getAddedSubList()) {
                        BooleanProperty p = getItemBooleanProperty(item);
                        if (p != null) {
                            p.set(true);
                        }
                    }
                }

                if (c.wasRemoved()) {
                    for (T item : c.getRemoved()) {
                        BooleanProperty p = getItemBooleanProperty(item);
                        if (p != null) {
                            p.set(false);
                        }
                    }
                }
            }
        });
    }

    @Override
    public abstract T getItem(int index);

    @Override
    public abstract int getItemCount();

    @Override
    public abstract int getItemIndex(T item);

    BooleanProperty getItemBooleanProperty(T item) {
        return itemBooleanMap.get(item);
    }
    /**
     * Returns a read-only list of the currently checked indices in the CheckBox.
     */
    @Override
    public ObservableList<Integer> getCheckedIndices() {
        return checkedIndicesList;
    }

    /**
     * Returns a read-only list of the currently checked items in the CheckBox.
     */
    @Override
    public ObservableList<T> getCheckedItems() {
        return checkedItemsList;
    }

    /** {@inheritDoc} */
    @Override
    public void checkAll() {
        for (int i = 0; i < getItemCount(); i++) {
            check(i);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void checkIndices(int... indices) {
        for (int index : indices) {
            check(index);
        }
    }

    /** {@inheritDoc} */
    @Override public void clearCheck(T item) {
        int index = getItemIndex(item);
        clearCheck(index);
    }

    /** {@inheritDoc} */
    @Override
    public void clearChecks() {
        for( int index = 0; index < checkedIndices.length(); index++) {
            clearCheck(index);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void clearCheck(int index) {
        if (index < 0 || index >= getItemCount()) {
            return;
        }
        checkedIndices.clear(index);

        final int changeIndex = checkedIndicesList.indexOf(index);
        checkedIndicesList.callObservers(new NonIterableChange.SimpleRemovedChange<>(changeIndex, changeIndex, index, checkedIndicesList));
    }

    /** {@inheritDoc} */
    @Override
    public boolean isEmpty() {
        return checkedIndices.isEmpty();
    }

    /** {@inheritDoc} */
    @Override public boolean isChecked(T item) {
        int index = getItemIndex(item);
        return isChecked(index);
    }

    /** {@inheritDoc} */
    @Override
    public boolean isChecked(int index) {
        return checkedIndices.get(index);
    }

    /** {@inheritDoc} */
    @Override
    public void toggleCheckState(T item) {
        int index = getItemIndex(item);
        toggleCheckState(index);
    }

    /** {@inheritDoc} */
    @Override
    public void toggleCheckState(int index) {
        if (isChecked(index)) {
            clearCheck(index);
        } else {
            check(index);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void check(int index) {
        if (index < 0 || index >= getItemCount()) {
            return;
        }
        checkedIndices.set(index);
        final int changeIndex = checkedIndicesList.indexOf(index);
        checkedIndicesList.callObservers(new NonIterableChange.SimpleAddChange<>(changeIndex, changeIndex+1, checkedIndicesList));
    }

    /** {@inheritDoc} */
    @Override
    public void check(T item) {
        int index = getItemIndex(item);
        check(index);
    }

    /***********************************************************************
     *                                                                     *
     * Private implementation                                              *
     *                                                                     *
     **********************************************************************/

    protected void updateMap() {
        // reset the map
        itemBooleanMap.clear();
        for (int i = 0; i < getItemCount(); i++) {
            final int index = i;
            final T item = getItem(index);

            final BooleanProperty booleanProperty = new SimpleBooleanProperty(item, "selected", false); //$NON-NLS-1$
            itemBooleanMap.put(item, booleanProperty);

            // this is where we listen to changes to the boolean properties,
            // updating the selected indices list (and therefore indirectly
            // the selected items list) when the checkbox is toggled
            booleanProperty.addListener(o -> {
                if (booleanProperty.get()) {
                    checkedIndices.set(index);
                    final int changeIndex = checkedIndicesList.indexOf(index);
                    checkedIndicesList.callObservers(new NonIterableChange.SimpleAddChange<>(changeIndex, changeIndex+1, checkedIndicesList));
                } else {
                    final int changeIndex = checkedIndicesList.indexOf(index);
                    checkedIndices.clear(index);
                    checkedIndicesList.callObservers(new NonIterableChange.SimpleRemovedChange<>(changeIndex, changeIndex, index, checkedIndicesList));
                }
            });
        }
    }
}